import {
  apply,
  applyTemplates,
  chain,
  MergeStrategy,
  mergeWith,
  move,
  Rule,
  SchematicContext,
  Tree,
  url,
} from '@angular-devkit/schematics';
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
import { resolveTree } from '../utils/testing';
import { normalize } from '@angular-devkit/core';


// You don't have to export the function as default. You can also have more than one rule factory
// per file.
export function ngAdd(_options: any): Rule {
  return (_tree: Tree, _context: SchematicContext) => {
    return chain([
      addDependencies(),
      addPackageMain(),
      addPackageScripts(),
      addFiles(_options),
      addPackageInstallTask(),
    ])(_tree, _context);
  };
}

const scripts = [
  {'postinstall': 'electron-builder install-app-deps'},
  {'electron': 'electron'},
  {'electron:serve': 'wait-on tcp:4200 && npm run electron:serve-tsc && electron . --serve'},
  {'electron:start': 'npm-run-all -p electron:serve start'},
  {'electron:serve-tsc': 'tsc -p tsconfig.electron.json'},
  {'electron:build': 'npm run electron:serve-tsc && electron-builder build --publish=never'},
  {'electron:build-mac': 'npm run electron:serve-tsc && electron-builder build --publish=never --mac'},
  {'electron:build-windows': 'npm run electron:serve-tsc && electron-builder build --publish=never --windows'},
  {'electron:build-linux': 'npm run electron:serve-tsc && electron-builder build --publish=never --linux'},
];

function addPackageMain(): Rule {
  return (_tree: Tree, _context: SchematicContext) => {
    let json = JSON.parse(_tree.readText('/package.json'));
    json.main = 'app/main.js';
    _tree.overwrite('/package.json', JSON.stringify(json, null, 2));
    return _tree;
  };
}

function addPackageScripts(): Rule {
  return (_tree: Tree, _context: SchematicContext) => {
    let json = JSON.parse(_tree.readText('/package.json'));
    let oldScripts = json.scripts;
    for (let s of scripts) {
      let key = Object.keys(s)[0];
      let value = Object.values(s)[0];
      if (!oldScripts.hasOwnProperty(key)) {
        oldScripts[key] = value;
      }
    }
    _tree.overwrite('/package.json', JSON.stringify(json, null, 2));
    return _tree;
  };
}

function addFiles(_options: any): Rule {
  return async (_tree: Tree, _context: SchematicContext) => {
    let resolve = await resolveTree(_tree);
    return mergeWith(apply(url('./files'), [
      applyTemplates({}),
      move(normalize(`${resolve.project.root}`)),
    ]), MergeStrategy.Overwrite);
  };
}

const devDependencies = [
  {name: 'electron', version: '*'},
  {name: 'electron-builder', version: '*'},
  {name: 'electron-debug', version: '*'},
  {name: 'electron-reloader', version: '*'},
  {name: 'npm-run-all', version: '*'},
  {name: 'wait-on', version: '*'},
];

function addDependencies() {
  return (_tree: Tree, _context: SchematicContext) => {
    devDependencies.forEach(d => {
      addDependencyToPackageJson(
          _tree,
          'devDependencies',
          d.name,
          d.version,
      );
    });
    return _tree;
  };
}

function addPackageInstallTask() {
  return (_tree: Tree, _context: SchematicContext) => {
    const devDepNames = devDependencies.map(d => d.name).join(' ');
    _context.addTask(
        new NodePackageInstallTask({
          packageName: devDepNames,
        }),
    );
    return _tree;
  };
}

/**
 * Adds a package to the package.json
 */
function addDependencyToPackageJson(
    tree: Tree,
    type: DependencyTypes,
    pkg: string,
    version: string,
): Tree {
  if (tree.exists('/package.json')) {
    const sourceText = tree.read('/package.json')!.toString('utf-8');
    const json = JSON.parse(sourceText);
    if (!json[type]) {
      json[type] = {};
    }

    if (!json[type][pkg]) {
      json[type][pkg] = version;
      json.dependencies = sortObjectByKeys(json.dependencies);
    }

    tree.overwrite('/package.json', JSON.stringify(json, null, 2));
  }

  return tree;
}

type DependencyTypes =
    | 'dependencies'
    | 'devDependencies'
    | 'optionalDependencies'
    | 'peerDependencies';

function sortObjectByKeys(obj: any): any {
  return Object.keys(obj)
      .sort()
      .reduce((result: any, key: string) => (result[key] = obj[key]) && result, {});
}
